package ch.ethz.fcl.metrobuzz.data.roadnetwork.hierarchy;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ch.ethz.fcl.metrobuzz.data.MBData;
import ch.ethz.fcl.metrobuzz.data.scene.basemap.Edge;
import ch.ethz.fcl.metrobuzz.data.scene.basemap.Node;
import ch.ethz.fcl.metrobuzz.tools.MBTools;

/**
 * sort road network into a hierarchy of strokes->arcs->edges->nodes
 * 
 * @author ZengWei
 * 
 */
public class RoadHierarchy {
	private MBData data;

	private List<Stroke> strokes;
	private List<Arc> arcs;

	private Map<Edge, Integer> edgeToArcIndexMap;
	private Map<Edge, Stroke> edgeToStrokeMap;

	private float bidirectionAngle = 0.99f;

	public RoadHierarchy(MBData data) {
		this.data = data;

		strokes = new ArrayList<Stroke>();
		arcs = new ArrayList<Arc>();

		edgeToArcIndexMap = new HashMap<Edge, Integer>();
		edgeToStrokeMap = new HashMap<Edge, Stroke>();
	}

	public void init() {
		sortEdgesIntoArcs();
		sortArcsIntoStroke();
	}

	private void sortEdgesIntoArcs() {
		boolean[] edge_sorted = new boolean[data.getRoadNetwork()
				.getPassingEdges().size()];
		int arc_id = 0;
		for (int i = 0; i < data.getRoadNetwork().getPassingEdges().size(); i++) {
			if (!edge_sorted[i]) {
				Arc arc = new Arc(data.getScene(), arc_id);
				arc.addEdge(data.getRoadNetwork().getPassingEdges().get(i), 0);
				edgeToArcIndexMap.put(data.getRoadNetwork().getPassingEdges()
						.get(i), arcs.size());

				expandArcs(arc, i, edge_sorted);

				arc_id++;
				arcs.add(arc);
			}
		}

		for (int i = 0; i < arcs.size(); i++)
			arcs.get(i).init();
	}

	private void expandArcs(Arc arc, int edge_index, boolean[] edge_sorted) {
		Edge edge = data.getRoadNetwork().getPassingEdges().get(edge_index);
		Node start = data.getScene().getNode(edge.getStartNodeIndex());

		ArrayList<Node> startNodes = data.getRoadNetwork().getStartNodes();
		ArrayList<Node> junctionNodes = data.getRoadNetwork()
				.getJunctionNodes();
		ArrayList<Integer> pre_edge_index = data.getRoadNetwork()
				.getEndEdgeIndex(start);

		if (!startNodes.contains(start) && !junctionNodes.contains(start)) {
			for (int i = 0; pre_edge_index != null && i < pre_edge_index.size(); i++) {
				if (edge.cos(data.getRoadNetwork().getPassingEdges()
						.get(pre_edge_index.get(i))) > bidirectionAngle
						&& !edge_sorted[pre_edge_index.get(i)]) {
					arc.addEdge(
							data.getRoadNetwork().getPassingEdges()
									.get(pre_edge_index.get(i)), 0);
					edge_sorted[pre_edge_index.get(i)] = true;
					edgeToArcIndexMap.put(data.getRoadNetwork()
							.getPassingEdges().get(pre_edge_index.get(i)),
							arcs.size());

					expandArcs(arc, pre_edge_index.get(i), edge_sorted);
				}
			}
		}

		Node end = data.getScene().getNode(edge.getEndNodeIndex());
		ArrayList<Integer> post_edge_index = data.getRoadNetwork()
				.getStartEdgeIndex(end);
		if (!startNodes.contains(end) && !junctionNodes.contains(end)) {
			for (int i = 0; post_edge_index != null
					&& i < post_edge_index.size(); i++) {
				if (edge.cos(data.getRoadNetwork().getPassingEdges()
						.get(post_edge_index.get(i))) > bidirectionAngle
						&& !edge_sorted[post_edge_index.get(i)]) {
					arc.addEdge(
							data.getRoadNetwork().getPassingEdges()
									.get(post_edge_index.get(i)), arc.size());
					edge_sorted[post_edge_index.get(i)] = true;
					edgeToArcIndexMap.put(data.getRoadNetwork()
							.getPassingEdges().get(post_edge_index.get(i)),
							arcs.size());

					expandArcs(arc, post_edge_index.get(i), edge_sorted);
				}
			}
		}
	}

	private void sortArcsIntoStroke() {
		boolean[] arc_sorted = new boolean[arcs.size()];

		int stroke_count = 0;
		for (int i = 0; i < arcs.size(); i++) {
			if (!arc_sorted[i]) {
				Stroke stroke = new Stroke(stroke_count);
				stroke.addArc(arcs.get(i), 0);

				expandStrokes(stroke, i, arc_sorted);

				for (Arc arc : stroke.getArcs()) {
					for (Edge edge : arc.getEdges())
						edgeToStrokeMap.put(edge, stroke);
				}

				stroke_count++;
				strokes.add(stroke);
			}
		}

		// initialize stroke attributes after finish
		for (int i = 0; i < strokes.size(); i++)
			strokes.get(i).init(data.getScene());

		MBTools.debug("Stroke size " + strokes.size(), false);
	}

	private void expandStrokes(Stroke stroke, int arc_index,
			boolean[] arc_sorted) {
		Arc arc = arcs.get(arc_index);

		Edge start_edge = arc.getStartEdge();
		Edge end_edge = arc.getEndEdge();

		int connected_start_edge_index = match(start_edge, true, false);
		if (connected_start_edge_index != -1) {
			int connected_arc_index = edgeToArcIndexMap.get(data
					.getRoadNetwork().getPassingEdges()
					.get(connected_start_edge_index));
			if (!arc_sorted[connected_arc_index]) {
				arc_sorted[connected_arc_index] = true;
				stroke.addArc(arcs.get(connected_arc_index), 0);

				expandStrokes(stroke, connected_arc_index, arc_sorted);
			}
		}

		int connected_end_edge_index = match(end_edge, false, false);
		if (connected_end_edge_index != -1) {
			int connected_arc_index = edgeToArcIndexMap.get(data
					.getRoadNetwork().getPassingEdges()
					.get(connected_end_edge_index));
			if (!arc_sorted[connected_arc_index]) {
				arc_sorted[connected_arc_index] = true;
				stroke.addArc(arcs.get(connected_arc_index), stroke.size());

				expandStrokes(stroke, connected_arc_index, arc_sorted);
			}
		}
	}

	/**
	 * @param edge
	 *            : current edge
	 * @param start
	 *            : if true check for start node; else check for end node
	 * @param verify
	 *            : if it is for verification purpose to make sure current edge
	 *            is also the most smooth edge for the return edge
	 * @return edge index of the most smooth connected edge
	 */
	private int match(Edge edge, boolean start, boolean verify) {
		float min_similarity = -10;
		int match_edge_index = -1;
		ArrayList<Integer> edges_index;
		if (start)
			edges_index = data.getRoadNetwork().getEndEdgeIndex(
					data.getScene().getNode(edge.getStartNodeIndex()));
		else
			edges_index = data.getRoadNetwork().getStartEdgeIndex(
					data.getScene().getNode(edge.getEndNodeIndex()));

		for (int i = 0; edges_index != null && i < edges_index.size(); i++) {
			float similarity = similarity(edge, data.getRoadNetwork()
					.getPassingEdges().get(edges_index.get(i)));
			if (similarity > min_similarity) {
				min_similarity = similarity;
				match_edge_index = edges_index.get(i);
			}
		}

		if (match_edge_index == -1)
			return -1;
		else if (!verify)
			if (start) {
				int index = match(
						data.getRoadNetwork().getPassingEdges()
								.get(match_edge_index), false, true);
				if (index != -1) {
					Edge pre_edge = data.getRoadNetwork().getPassingEdges()
							.get(index);
					if (pre_edge.getEndNodeIndex() != edge.getEndNodeIndex())
						return -1;
				}
			} else {
				int index = match(
						data.getRoadNetwork().getPassingEdges()
								.get(match_edge_index), true, true);
				if (index != -1) {
					Edge post_edge = data.getRoadNetwork().getPassingEdges()
							.get(index);
					if (post_edge.getStartNodeIndex() != edge
							.getStartNodeIndex())
						return -1;
				}
			}

		return match_edge_index;
	}

	/**
	 * measure the similarity between two edges taking the density and angle
	 * into consideration
	 * 
	 * @param e1
	 * @param e2
	 * @return
	 */
	private float similarity(Edge e1, Edge e2) {
		float density_similarity = 1;
		float angle_similarity = e1.cos(e2);

		if (e1.getBidirection() != null
				&& e1.getBidirection().getId() == e2.getId())
			return -10.0f;

		return density_similarity + angle_similarity;
	}

	public Arc getArc(Edge edge) {
		return arcs.get(edgeToArcIndexMap.get(edge));
	}

	public List<Arc> getArcs() {
		return arcs;
	}
}
